
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Data;
using Red.Core;
using Red.Core.Containers;
using Red.EngineData.View;

namespace Red.Controls.ViewModels
{
    /// <summary>
    ///     UI wrapper for Engine Data View
    /// </summary>
    public class InteropDataItem : DataItem
    {
        private readonly string _group;

        protected readonly Dictionary<string, Notifier> _notifiers = new Dictionary<string, Notifier>();
        protected Notifier _onChangedNotifier;
        protected List<CommandWrapper> _commands = new List<CommandWrapper>();
        protected readonly DataView _view;

        #region C-tor

        /// <summary>
        ///     Creates root DataItem (with no parent) wrapping given DataView and expands it.
        /// </summary>
        public static void CreateRoot( DataView view, Action<InteropDataItem> onCreated, bool rebuildOnChange = true )
        {
            view.Refresh(
                ex =>
                {
                    var item = new InteropDataItem( null, view, string.Empty, ex, rebuildOnChange );
                    if ( ex == null )
                    {
                        item.IsExpanded = true;
                    }
                    onCreated( item );
                } );
        }

        /// <summary>
        ///     Creates root DataItem (with no parent) wrapping given DataView and expands it - awaitable version
        /// </summary>
        public static Task<InteropDataItem> CreateRoot( DataView view, bool rebuildOnChange = true )
        {
            var tcs = new TaskCompletionSource<InteropDataItem>();
            CreateRoot( view, item => tcs.SetResult( item ), rebuildOnChange );
            return tcs.Task;
        }

        // NOTE: This constructor assumes the data is already synchronized. It's private and not intended for an external use. 
        // You should either use CreateRoot factory method (for root items) or DigUpSubItem (for any descendants).
        private InteropDataItem( DataItem parent, DataView view, string group, Exception exception = null, bool rebuildOnChange = false )
            : base( parent, ExtractMessageFromException( exception ), true )
        {
            _view = view;
            _group = group;
            Title = _view.InteropName;
            IsReadOnly = _view.IsReadOnly;

            _view.RequestNotifier( "_onChanged",
                notifier =>
                {
                    _onChangedNotifier = notifier;
                    _onChangedNotifier.OnNotifyEvent += ChangedNotifier_OnNotifyEvent;
                } );

            RebuildOnChange = rebuildOnChange;

            if ( _view.IsCompound )
            {
                PropertyGroupDescription groupDescription = new PropertyGroupDescription( "Group" );
                ChildrenView.GroupDescriptions.Add( groupDescription );
            }
        }

        protected InteropDataItem( DataItem parent, DataView view, string title, string toolTip, Canvas image )
            : base( parent, "", true )
        {
            _view = view;

            Title = title;
            ToolTip = toolTip;
            Image = image;
        }

        #endregion

        #region Properites

        public DataView Data
        {
            get { return _view; }
        }

        public override string Type
        {
            get { return _view.Type; }
        }

        public override DataItemKind Kind
        {
            get
            {
                if ( _view.IsSimpleValue )
                    return DataItemKind.Simple;
                else if ( _view.IsCompound )
                    return DataItemKind.Compound;
                else if ( _view.IsArray )
                    return DataItemKind.Container;
                else if ( _view.IsCallable || _view.IsNotifier )
                    return DataItemKind.Hidden;
                else
                    return DataItemKind.Unknown;
            }
        }

        public override string Group
        {
            get { return _group; }
        }

        public override string Value
        {
            get { return _view.Value; }

            set
            {
                if ( value != _view.Value )
                {
                    _view.SetValue( value,
                        () =>
                        {
                            HandleDataOk();
                            if ( _onChangedNotifier == null )
                                OnPropertyChanged();
                        },
                        ( ProtocolException ex ) =>
                        {
                            HandleDataError( ExtractMessageFromException( ex ) );
                            if ( _onChangedNotifier == null )
                                OnPropertyChanged();
                        } );
                }
            }
        }

        public override string Path
        {
            get { return _view.Path; }
        }

        public override IEnumerable<CommandWrapper> Commands
        {
            get { return _commands.AsReadOnly(); }
        }

        #endregion

        #region Public methods

        public override Task<bool> RegisterNotifierAsync( string name, Action<DataItem, string> onNotification )
        {
            var tcs = new TaskCompletionSource<bool>();
            RequestNotifier( name,
                notifier =>
                {
                    notifier.OnNotifyEvent += x => onNotification( this, x );
                    tcs.SetResult( true );
                },
                ex =>
                {
                    tcs.SetResult( false );
                } );
            return tcs.Task;
        }

        #endregion Public methods

        #region Protected methods

        protected override async Task<DataItem> ProvideUnlistedChild( string path )
        {
            var childView = _view.GetSubView( path );
            var tcs = new TaskCompletionSource<DataItem>();
            // NOTE: see comment for childView.Refresh in EnumerateChildrenAsync
            childView.Refresh( ex =>
            {
                var item = new InteropDataItem( this, childView, null, ex );
                tcs.SetResult( item );
            } );
            return await tcs.Task;
        }

        protected override string SplitPath( string path, out string theRest )
        {
            return DataView.ExtractPathSegment( path, out theRest );
        }

        protected override string CombinePath( string pathA, string pathB )
        {
            return DataView.CombinePath( pathA, pathB );
        }

        protected async override Task EnumerateChildrenAsync( IRangedCollection<DataItem> children )
        {
            if ( _view.Children.Count == 0 || _view.IsCallable || _view.IsNotifier )
            {
                children.Clear();
                return;
            }

            var tcs = new TaskCompletionSource<bool>();

            var childrenNames = new string[ _view.Children.Count ];
            _view.Children.CopyTo( childrenNames, 0 );

            var newChildren = new DataItem[ _view.Children.Count ];

            for ( int i = 0; i < childrenNames.Length; ++i )
            {
                string childName = _view.Children[i];
                string groupName = _view.Categories[i]; // Children.Count == Categories.Count
                DataView childView = _view.GetSubView( childName );

                int capturedIdx = i;
                // NOTE: using awaitable Refresh() will be more convenient (no need to TaskCompletionSource and stuff), but much slower
                // due to better-performing scheduler used in the callback-based version
                childView.Refresh( ex =>
                {
                    newChildren[capturedIdx] = new InteropDataItem( this, childView, groupName, ex );
                    if ( capturedIdx == childrenNames.Length - 1 )
                    {
                        children.ResetWith( newChildren );
                        // we run out of children, so we can continue now
                        tcs.SetResult( true );
                    }
                } );
            }

            await tcs.Task;
        }

		protected override Task<Result<string>> ExecuteAsyncInternal( string funcName, params object[] callArgs )
        {
            var tcs = new TaskCompletionSource<Result<string>>();
            _view.Execute( Interop.MakeCall( funcName, callArgs ),
                res => tcs.SetResult( Result<string>.Ok( res.Data ) ),
                ex => tcs.SetResult( Result<string>.Failed( ex.Message ) )
                );
            return tcs.Task;
        }

        protected override async Task<Result> FetchAndValidateAsync()
        {
            var exception = await _view.Refresh();
            if ( exception != null )
                return Result.Failed( exception.Message );
            else
                return Result.Ok;
        }

        #endregion

        #region Private methods

        private static string ExtractMessageFromException( Exception exception )
        {
            if ( exception == null )
            {
                return string.Empty;
            }

            StringBuilder errorMessage = new StringBuilder();

            AggregateException aggregateException = exception as AggregateException;
            if ( aggregateException != null )
            {
                for ( int i = 0; i < aggregateException.InnerExceptions.Count; ++i )
                {
                    if ( i != 0 && i != aggregateException.InnerExceptions.Count )
                    {
                        errorMessage.Append( "; " );
                    }
                    errorMessage.Append( aggregateException.InnerExceptions[i].Message );
                }
            }
            else
            {
                errorMessage.Append( exception.Message );
            }

            return errorMessage.ToString();
        }

        private void RequestNotifier( string name, Action<Notifier> onSuccess, Action<ProtocolException> onException )
        {
            Notifier existingNotifier;
            if ( _notifiers.TryGetValue( name, out existingNotifier ) )
            {
                onSuccess( existingNotifier );
            }
            else
            {
                _view.RequestNotifier( name,
                    newNotifier =>
                    {
                        _notifiers.Add( name, newNotifier );
                        onSuccess( newNotifier );
                    },
                    ex =>
                    {
                        onException( ex );
                    } );
            }
        }

        private async void ChangedNotifier_OnNotifyEvent( string path )
        {
            if ( string.IsNullOrEmpty( path ) ) // this check ensures the notifier was called for _this_ DataView, not one of its children
            {
                if ( RebuildOnChange )
                    await RebuildAsync();
                else
                    await RefreshAsync();
            }
        }
        #endregion
    }
}
